# CD Player Plugin for SqueezeCenter

# Copyright (C) 2008 Bryan Alton and others
# All rights reserved.

# This program is free software; you can redistribute it and/or
# modify it under the same terms as Perl itself.

package Plugins::CDplayer::CDPLAY;

use strict;

use base qw(Slim::Player::Pipeline);
#use utf8;

use Slim::Utils::Strings qw(string);
use Slim::Utils::Misc;
use Slim::Utils::Log;
use Slim::Utils::Prefs;
use URI::Escape;
use HTML::Entities;
use XML::Simple;
use Tie::Cache::LRU;
use Digest::SHA::PurePerl qw(hmac_sha256_base64);

use Data::Dumper;

my $log   = logger('plugin.cdplayer');
my $prefs = preferences('plugin.cdplayer');
my $osdetected = Slim::Utils::OSDetect::OS();

use constant CDPLAYING => 1;
use constant CDSTOPPED => 0;


Slim::Player::ProtocolHandlers->registerHandler('cdplay', __PACKAGE__);

#  Hash holds any URL for Amazon album art  - keyed on searchstring. 
tie my %urlimg, 'Tie::Cache::LRU', 32;


sub canDoAction {
	my ( $class, $client, $url, $action ) = @_;
	$log->info("Action=$action");
	if (($action eq 'pause') && $prefs->get('pausestop') ) {
		$log->info("Stopping track because pref is set yo stop");
		return 0;
	}
	
	return 1;
}

# use a small buffer threshold to make CD start playing quickly 

sub bufferThreshold { 70 }

sub isRemote { 1 }

sub new {
	my $class = shift;
	my $args  = shift;

	my $fullurl      = $args->{'url'};
	my $client       = $args->{'client'};
	my $transcoder   = $args->{'transcoder'};
	my $song         = $args->{'song'};
	my $command;

	my $seekdata;
	if ($song->can("seekdata")) {
		$seekdata     = $song->seekdata();
	} else {
		$seekdata     = $args->{'song'}->{'seekdata'};
	}

	my $offsetSectors;
	
	if (defined($seekdata)) {
		$log->debug("Seekdata \n". Dumper($seekdata));
	}

	if (defined ($seekdata->{timeOffset})) {
		my $newtime = int($seekdata->{timeOffset});
		$offsetSectors = int($seekdata->{timeOffset} * 75) ; # 75 sectors in a second.

		if ($client->playingSong->can('startOffset')) {
			$client->playingSong->startOffset( $newtime);
		} else {
			$client->playingSong->{startOffset} = $newtime;
		};

		$client->master->remoteStreamStartTime( Time::HiRes::time() - $newtime );
	}


	my $cdInfo = Plugins::CDplayer::Plugin::cdInfo();
	my $cddevice; 
	if ($osdetected eq 'win') { # Use drop downlist built at startup
		$cddevice = $prefs->get('cddevice');
	} elsif ($osdetected eq 'unix') { # use text box
		$cddevice = $prefs->get('device');
	} else { # use drop down box filled with predefined OSX devcies name for CD/DVD drive.
		$cddevice = $prefs->get('cddevice');
	}

	$log->debug(" Host OS = $osdetected  CD device name=\'$cddevice\'");
	my $client  = $args->{'client'};
#	my $fullurl = $args->{'url'} ;
	my ($baseurl,$params) = split (/\?/,$fullurl); 
	$baseurl =~ m|^cdplay://(\d+)|;	
	my $tracknum = $1;

	my $restoredurl;

	$log->debug("params length =". length ($params) ."\n" . Data::Dumper::Dumper($params));

	my %ops = map {
			my ( $k, $v ) = split( '=' );
#			$k  => Slim::Utils::Unicode::utf8decode_locale(uri_unescape( $v ))
			$k  => Encode::decode_utf8(uri_unescape( $v ))
		} split( '&', $params );

	Slim::Music::Info::setContentType($fullurl, 'cdplay');

#	my ($command, $type, $format) = Slim::Player::TranscodingHelper::getConvertCommand($client, $fullurl);


#	unless (defined($command) && $command ne '-') {
#		$log->warn("Couldn't find conversion command for $fullurl");
#		Slim::Player::Source::errorOpening($client, string('PLUGIN_CDPLAYER_NO_CONVERT_CMD'));
#		return undef;
#	}

	$log->debug("Full URL= $fullurl Format=" . $transcoder->{'streamformat'});

#	Slim::Music::Info::setContentType($fullurl, $format);

	my $maxRate = 0;
	my $quality = 1;

	if (defined($client)) {
		$maxRate = Slim::Utils::Prefs::maxRate($client);
		$quality = preferences('server')->client($client)->get('lameQuality');
	}

	$restoredurl = $baseurl;
	my $cdspeed = 4;

	if (defined($offsetSectors)) {
		$transcoder->{'command'} =~ s/\$CDOFFSET\$/-offset $offsetSectors/;
	} else {
		$transcoder->{'command'} =~ s/\$CDOFFSET\$//;
	}

	$transcoder->{'command'} =~ s/\$CDTRACK\$/$tracknum/;
	$transcoder->{'command'} =~ s/\$CDDEVICE\$/$cddevice/;
	$transcoder->{'command'} =~ s/\$CDSPEED\$/$cdspeed/;

	$command = Slim::Player::TranscodingHelper::tokenizeConvertCommand2( $transcoder, $fullurl, $fullurl, 1, $quality );

#	$command = Slim::Player::TranscodingHelper::tokenizeConvertCommand($command, $type, $restoredurl, $baseurl, 0, $maxRate, 1, $quality);
$log->debug("about to execute:$command");

#	if ($osdetected eq 'win') {
#		$command =~ s|\"cdplay://(\d+)\"| device=$cddevice -track $1|;

#	} elsif ($osdetected eq 'mac') {
#		$command =~ s|\"cdplay://(\d+)\"|  device=$cddevice -track $1|;

#	} else {
#		$command =~ s|\"cdplay://(\d+)\"|  device=$cddevice -track $1|;
#	}

	$cdInfo->{PlayingAlbumTitle}   =  $ops{AlbumTitle}    || Slim::Utils::Strings::string('PLUGIN_CDPLAYER_UNKNOWN_ALBUM'); 
	$cdInfo->{PlayingAlbumArtist}  =  $ops{AlbumArtist}   || Slim::Utils::Strings::string('PLUGIN_CDPLAYER_UNKNOWN_ARTIST') ;
	$cdInfo->{PlayingTrackTitle}   =  $ops{TrackTitle}    || Slim::Utils::Strings::string('PLUGIN_CDPLAYER_UNKNOWN_TRACK') ;
	$cdInfo->{PlayingTrackArtist}  =  $ops{TrackArtist} ;
	$cdInfo->{PlayingLengths}      =  $ops{Lengths} ;
	$cdInfo->{PlayingOffsets}      =  $ops{Offsets} ;
	$cdInfo->{PlayingMBDiscid}     =  $ops{MBDiscid};



	$log->info("SetCurrentTitle for $fullurl ");
	Slim::Music::Info::setCurrentTitle( $fullurl, $cdInfo->{PlayingTrackTitle} . ' (' . $cdInfo->{durations}[$tracknum] .')' );
	Slim::Music::Info::setDuration( $fullurl, (int($cdInfo->{PlayingLengths})/75) );

	$cdInfo->CDplaying ( CDPLAYING );

	startCustomAmazonSearch( $class, $cdInfo); 


	my $self = $class->SUPER::new(undef, $command,'local');

	${*$self}{'contentType'} = $transcoder->{'streamformat'};
#	${*$self}{'contentType'} = $format;

	return $self;
}

sub contentType 
{
	my $self = shift;
	return ${*$self}{'contentType'};
}


sub canHandleTranscode {
	my ($self, $song) = @_;
	
	return 1;
}

sub getStreamBitrate {
	my ($self, $maxRate) = @_;
	
	return Slim::Player::Song::guessBitrateFromFormat(${*$self}{'contentType'}, $maxRate);
}


sub isAudioURL { 1 }

sub OnCommandCDInfoCallback()
{
	my $client = shift;

	my $foundDiscId = shift;
	my $callback = $client->pluginData( 'onCommandCallback' );

	$log->debug("OnCommandCallback callback=$callback");
	return $callback->();
}

sub close 
{
	my $class = shift;
	$log->debug("closing cdda2wav stream to SC ");
	my $cdInfo = Plugins::CDplayer::Plugin::cdInfo();

	my $self = $class->SUPER::close();
	$cdInfo->CDplaying ( CDSTOPPED );

	$cdInfo->killOrphans();

}

# Metadata for a URL, used by CLI/JSON clients
sub getMetadataFor {
	my ( $class, $client, $url, $forceCurrent ) = @_;

	my $cdInfo = Plugins::CDplayer::Plugin::cdInfo();
	my $icon   = Plugins::CDplayer::Plugin->_pluginDataFor('icon'); 

	if (defined($urlimg{$cdInfo->{SearchString}})) {
		$icon = $urlimg{$cdInfo->{SearchString}};
	} 

	my ($baseurl,$params) = split (/\?/,$url); 

	my %ops = map {
			my ( $k, $v ) = split( '=' );
			$k  => Encode::decode_utf8(uri_unescape( $v ))
		} split( '&', $params );


	return {
		artist   =>  $ops{TrackArtist}  || $ops{AlbumArtist} || Slim::Utils::Strings::string('PLUGIN_CDPLAYER_UNKNOWN_ARTIST') ,
		album    =>  $ops{AlbumTitle}                        || Slim::Utils::Strings::string('PLUGIN_CDPLAYER_UNKNOWN_ALBUM')  ,
		title    =>  $ops{TrackTitle}                        || Slim::Utils::Strings::string('PLUGIN_CDPLAYER_UNKNOWN_TRACK')  ,
		duration =>  int($ops{Lengths})/75,
		type     =>  'CD',
		icon     =>  $icon ,
		cover    =>  $icon,
	};
}

# XXX - I think that we scan the track twice, once from the playlist and then again when playing
sub scanUrl {
	my ( $class, $url, $args ) = @_;
	
	Slim::Utils::Scanner::Remote->scanURL($url, $args);

}



sub canSeek {
	my ( $class, $client, $song ) = @_;
	
# Can only seek if duration is known
	my $seconds = $song->duration();

	if ($seconds) {
	  return 1;
	}

	$log->error("Cannot seek duration ($seconds) may be undefined or 0");

	return 0;
}

sub canSeekError {
	my ( $class, $client, $song ) = @_;
	
	my $url = $song->currentTrack()->url;
	
	if ($log->is_debug) {
		my $ct = Slim::Music::Info::contentType($url);
		$log->debug( " CanSeekError content type $ct url=$url " );
	}

	if ( !$song->duration() ) {
		return 'SEEK_ERROR_CDPLAY_UNKNOWN_DURATION';
	}
	
	return 'SEEK_ERROR_CDPLAY';
}

sub getSeekData {

	my ( $class, $client, $song, $newtime ) = @_;
	
	# Do it all later at open time
	return {timeOffset => $newtime};
}


my @amazonlocale =  ('.com', '.co.uk' , '.de', '.fr', '.ca');
my $amazontimeout =  30;



#  New required signing - see http://docs.amazonwebservices.com/AWSECommerceService/2009-03-31/DG/index.html?Query_QueryAuth.html
#
sub sign_query {

	my $uri        = shift;
	my @now        = gmtime;

	my %query      = $uri->query_form;

	$query{Timestamp} ||= sprintf('%04d-%02d-%02dT%02d:%02d:%02dZ',$now[5]+1900,$now[4]+1,@now[3,2,1,0]);
	my $qstring = join '&', map {"$_=". uri_escape($query{$_},"^A-Za-z0-9\-_.~")} sort keys %query;
	my $signme  = join chr(10),"GET",$uri->host,$uri->path,$qstring;
	my $sig     = hmac_sha256_base64($signme, $prefs->get('secretkey'));

#	Digest does not properly pad b64 strings

	$sig     .= '=' while length($sig) % 4;
	$sig      = uri_escape($sig,"^A-Za-z0-9\-_.~");
	$qstring .= "&Signature=$sig";
	$log->info("Amazon query string=$qstring");
	$uri->query( $qstring );

	return $uri->as_string;
}


sub startCustomAmazonSearch 
{
	my $class        = shift;
	my $cdInfo 	 = shift;
	
	my $titlesearch  = $cdInfo->{PlayingAlbumTitle};
	my $artistsearch = $cdInfo->{PlayingAlbumArtist};

#	my $ptr   = getDataPtr($client);
	my $searchstring;
	$log->info("Title=\"$titlesearch\"\n". sprintf("%vx",$titlesearch));
	$titlesearch =~ s/[\p{IsLm}]//g;
	$titlesearch =~ s/[^'[:alnum:]]/ /g;
	$titlesearch =~ s/^\s+//;
	$artistsearch =~ s/^\s+//;
	$log->info("Title=\"$titlesearch\"\n". sprintf("%vx",$titlesearch));


	$log->debug("Start Custom Amazon search");
	
	$searchstring =  'Title='. URI::Escape::uri_escape_utf8($titlesearch) ;
	$searchstring .= '&Artist='. URI::Escape::uri_escape_utf8($artistsearch) ;
	$cdInfo->{SearchString} = $searchstring; 

	if (defined($urlimg{$cdInfo->{SearchString}}))  {
		my $icon = $urlimg{$cdInfo->{SearchString}};
		$log->info("CD cover for \"$searchstring\" already found=\"$icon");
		return ;
	} 

	$log->debug("Search string=\"$searchstring\"");
## See Amazon Developer Guide for URL syntax http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/

#	my $url = 'http://ecs.amazonaws' . @amazonlocale[$prefs->get('amazonlocale')]  .
#	      "/onca/xml?Service=AWSECommerceService&Version=2007-10-29&Operation=ItemSearch&AssociateTag=webservices-20&AWSAccessKeyId=11ZKJS8X1ETSTJ6MT802&$searchstring&SearchIndex=Music&ResponseGroup=Small,Images";

# See Amazon Developer Guide for URL syntax http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/
	my $url = 'http://ecs.amazonaws' . @amazonlocale[$prefs->get('amazonlocale')] .
			 "/onca/xml?Service=AWSECommerceService&Version=2007-10-29&Operation=ItemSearch&AssociateTag=webservices-20&AWSAccessKeyId=".$prefs->get('accesskeyid')."&$searchstring&SearchIndex=Music&ResponseGroup=Small,Images";

	my $rooturl = URI->new($url) ;
	my $signedurl = sign_query($rooturl);
	$log->info("Amazon search signed url ".$signedurl);


	my $http = Slim::Networking::SimpleAsyncHTTP->new(
				\&searchAmazonOK, \&searchAmazonError,
				{
					timeout => $amazontimeout,
					class => $class,
					cdinfo => $cdInfo,
				} );
	$http->get($signedurl);

}

sub searchAmazonOK
{
	my $http    = shift;
	my $content = $http->content;
	my $class   = $http->params('client');
	my $cdInfo  = $http->params('cdinfo');

	$log->debug("Received search results from Amazon");

	my $asinrec = XMLin( $content , 'forcearray' => [qw(Item Creator Artist)], 'keyattr' => []);

#	$log->debug("Request Arguement\n ". Dumper($asinrec->{OperationRequest}->{Arguments}));
	my $totalresults = int ($asinrec->{'Items'}->{'TotalResults'});

	if ($totalresults > 0 ) {
		my $items = $asinrec->{'Items'}->{'Item'};
		my $count = scalar @{$items};
		my $item  = @{$items}[0]; 

		$log->debug("Total matches =$totalresults No of items = ". scalar(@$items) );
		my $itemattributes    = $item->{ItemAttributes};
		my $itemartist        = getAmazonArtistInfo($itemattributes);
		my $itemtitle         = $itemattributes->{Title};
		my $itemheader        = $itemtitle . "\n". $itemartist;
		my $itemLargeImageURL = defined ($item->{LargeImage}->{URL} ) ? $item->{LargeImage}->{URL} : undef;
		$log->info("Found image url:$itemLargeImageURL"); 
		if (defined ($item->{LargeImage}->{URL} )) {
			$urlimg{$cdInfo->{SearchString}} = $itemLargeImageURL;
		} 
	}
	else {
		$log->debug("Total matches =$totalresults Message :" . 
		$asinrec->{'Items'}->{'Request'}->{'Errors'}->{'Error'}->{'Message'});
	};

}

sub getAmazonArtistInfo
{
	my $itemattributes = shift;

	my @artists;
	my $creatoritem;

	@artists =  @{$itemattributes->{Artist}} if (defined ($itemattributes->{Artist})) ;

	if (defined($itemattributes->{Creator} )) {
		$creatoritem = $itemattributes->{Creator};
		foreach my $entry (@$creatoritem) {
			push @artists,  $entry->{content} . "(" . $entry->{Role} .")" ;
		}; 
	}

	return join('; ',@artists) if (defined(@artists)) ;
	return ' ';
}

sub searchAmazonError
{
  my $http    = shift;
  my $client  = $http->params('client');

  $log->error('Amazon http failed:' .$http->error);

}

1;

# Local Variables:
# tab-width:4
# indent-tabs-mode:t
# End:
